Skip to content

feat: Implement ALT + F4 and Window X button to close game#2336

Open
githubawn wants to merge 21 commits intoTheSuperHackers:mainfrom
githubawn:fix/graceful-exit-logic
Open

feat: Implement ALT + F4 and Window X button to close game#2336
githubawn wants to merge 21 commits intoTheSuperHackers:mainfrom
githubawn:fix/graceful-exit-logic

Conversation

@githubawn
Copy link

@githubawn githubawn commented Feb 21, 2026

Fixes #1356

Split up in 2 commits.

  1. Previously, the quit logic was duplicated and inconsistent across several places (the quit menu, the "quit to desktop" button, and the Alt+F4 handler). This commit centralizes all of that into a single GameLogic::quit(Bool toDesktop) method. It properly handles edge cases like stopping an active recording, sending a self-destruct message in multiplayer (using a new m_quitToDesktopAfterMatch flag), unpausing/unfreezing the game before exiting. All the old scattered quit code is replaced with a simple call to TheGameLogic->quit(true/false).

  2. This follow-up extends the graceful exit to two more situations where the game previously couldn't be closed cleanly:
    During the loading screen: The load progress update now checks if a quit has been requested and throws a QuitGameException, which is caught in startNewGame() to cleanly abort the loading process mid-way.
    During movies: The inline ESC-key checking that was copy-pasted in multiple video playback loops is refactored into a new GameClient::isMovieAbortRequested() method. This method now also checks for window close / Alt+F4 events (not just ESC), so closing the window during a movie no longer gets stuck. The MSG_META_DEMO_INSTANT_QUIT message is also registered as a system message so it propagates correctly even during loading.

@greptile-apps
Copy link

greptile-apps bot commented Feb 21, 2026

Greptile Summary

This PR centralises all quit logic into a single GameLogic::quit(Bool toDesktop, Bool force) method and extends graceful exit to two previously stuck situations: mid-loading (via QuitGameException thrown from updateLoadProgress() and caught in startNewGame()) and during movie playback (via the new static GameClient::isMovieAbortRequested() helper). MSG_META_DEMO_INSTANT_QUIT is promoted from a debug-only message to an always-available system message so it propagates correctly during loading. The previously duplicated and inconsistent quit paths in QuitMenu.cpp, CommandXlat.cpp, and the two WinMain.cpp files are all reduced to clean delegate calls.

Key changes:

  • GameLogic::quit() consolidates recorder stop, game-pause reset, script-engine unfreeze, multiplayer self-destruct, and desktop/main-menu branching into one place
  • startNewGame() wraps tryStartNewGame() in a try/catch for QuitGameException to cleanly abort mid-load
  • GameClient::isMovieAbortRequested() replaces copy-pasted ESC-key polling in every video loop and now also handles window-close/Alt+F4 events
  • WM_CLOSE in both WinMain.cpp files now posts MSG_META_DEMO_INSTANT_QUIT with a boolean force argument instead of hard-exiting

Note: The isAltF4 flag computed via GetAsyncKeyState in the WM_CLOSE handler is unreliable — by the time WM_CLOSE is dequeued, the keys are typically already released, so force will nearly always be false during gameplay. This means Alt+F4 in an active game shows the quit confirmation menu rather than bypassing it, but all loading-screen and movie-playback abort paths work correctly regardless of this flag.

Confidence Score: 4/5

  • PR is on the happy path to merge; the one remaining concern (GetAsyncKeyState timing) is a P2 and does not break the primary loading/movie-abort use cases.
  • Most previously raised concerns have been addressed: m_quitToDesktopAfterMatch is private with a const getter, tryStartNewGame is private, !isInSkirmishGame() guard is present, frameDecompress guard added for ChallengeLoadScreen, WM_QUERYENDSESSION null guard fixed in GeneralsMD, and the enum-shift concern was confirmed safe. The one open P2 is the GetAsyncKeyState timing issue for Alt+F4 detection, which only affects the quit-menu-vs-force-quit behaviour during normal gameplay and does not regress loading or movie abort.
  • Generals/Code/Main/WinMain.cpp and GeneralsMD/Code/Main/WinMain.cpp — isAltF4 detection logic.

Important Files Changed

Filename Overview
Core/GameEngine/Source/GameClient/GUI/LoadScreen.cpp Refactors all inline ESC-key / serviceWindowsOS calls in video playback loops to use the new GameClient::isMovieAbortRequested(). Adds the early-return guard after the ChallengeLoadScreen min-spec frame-ready wait to prevent frameDecompress on a non-ready frame. The SinglePlayerLoadScreen #if RTS_GENERALS min-spec wait loop (line ~599) still has no abort check, but frames complete quickly there so the impact is minimal.
Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Adds the central quit() method, wraps startNewGame() with a try/catch for QuitGameException, and adds the QuitGameException throw in updateLoadProgress(). The quit() method correctly restores the !isInSkirmishGame() guard for MSG_SELF_DESTRUCT and guards TheScriptEngine calls with loading-state checks. Logic is sound.
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Mirror of the Generals GameLogic.cpp changes. Identical quit() implementation, QuitGameException mechanism, and startNewGame() wrapper. Parity with the Generals version is maintained.
Generals/Code/Main/WinMain.cpp WM_CLOSE now posts MSG_META_DEMO_INSTANT_QUIT with a boolean force argument instead of hard-exiting. Adds proper null guards for TheGameEngine. The isAltF4 detection via GetAsyncKeyState is unreliable at dispatch time and will nearly always be false in practice, meaning Alt+F4 during gameplay shows the quit menu rather than force-quitting.
GeneralsMD/Code/Main/WinMain.cpp WM_CLOSE and WM_QUERYENDSESSION both updated to append MSG_META_DEMO_INSTANT_QUIT with a boolean argument and add null guards. Same GetAsyncKeyState reliability concern as the Generals version. WM_QUERYENDSESSION now guards TheGameEngine access in the else branch, addressing the prior null-dereference concern.
Generals/Code/GameEngine/Source/GameClient/GameClient.cpp Implements isMovieAbortRequested(): checks ESC key, then calls serviceWindowsOS()/propagateMessages() and checks quitting/quit-to-desktop flags. Also adds the abort check in the 4-second intro movie loop. Clean consolidation of previously duplicated logic.

Sequence Diagram

sequenceDiagram
    participant OS as Windows OS
    participant WndProc
    participant MsgStream as MessageStream
    participant CmdXlat as CommandTranslator
    participant GL as GameLogic::quit()
    participant LS as LoadScreen::update()

    Note over OS,LS: Alt+F4 / Window X during loading
    OS->>WndProc: WM_CLOSE
    WndProc->>MsgStream: appendMessage(MSG_META_DEMO_INSTANT_QUIT, force)
    Note over WndProc: isAltF4 via GetAsyncKeyState (may be unreliable)

    LS->>LS: serviceWindowsOS()
    LS->>MsgStream: propagateMessages()
    MsgStream->>CmdXlat: translateGameMessage()
    CmdXlat->>GL: quit(toDesktop=TRUE, force)
    GL->>GL: m_quitToDesktopAfterMatch = TRUE
    GL-->>CmdXlat: return
    CmdXlat-->>MsgStream: DESTROY_MESSAGE
    MsgStream-->>LS: return
    LS->>LS: check getQuitting() || isQuitToDesktopRequested()
    LS-->>GL: updateLoadProgress() throws QuitGameException
    GL->>GL: catch QuitGameException in startNewGame()
    Note over GL: Loading cleanly aborted

    Note over OS,LS: Alt+F4 / Window X during movie
    OS->>WndProc: WM_CLOSE
    WndProc->>MsgStream: appendMessage(MSG_META_DEMO_INSTANT_QUIT, force)
    loop isMovieAbortRequested() poll
        LS->>LS: serviceWindowsOS() + propagateMessages()
        LS->>GL: isQuitToDesktopRequested() → TRUE
        LS-->>LS: break out of movie loop
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Generals/Code/Main/WinMain.cpp
Line: 376-378

Comment:
**Alt+F4 detection via `GetAsyncKeyState` is unreliable**

`GetAsyncKeyState` polls the instantaneous key state at the time of the call. However, `WM_CLOSE` is *posted* (not sent) to the message queue by `DefWindowProc` in response to `WM_SYSKEYDOWN` for Alt+F4. By the time the pump dequeues and dispatches `WM_CLOSE`, the user has almost certainly already released both keys, so `altDown && f4Down` evaluates to `false` in the vast majority of real presses.

In practice this means `isAltF4` is nearly always `false` during active gameplay, so Alt+F4 will show the quit-confirmation menu rather than force-quitting — identical to the window X-button path. The same issue is present in `GeneralsMD/Code/Main/WinMain.cpp` at the equivalent lines.

A more reliable approach is to track the key-combination in a flag set during `WM_SYSKEYDOWN` (before `WM_CLOSE` is enqueued):

```cpp
// near the top of WndProc or in a static:
static Bool s_altF4Requested = FALSE;

case WM_SYSKEYDOWN:
    if (wParam == VK_F4 && (lParam & (1 << 29)))  // bit 29 = ALT held
        s_altF4Requested = TRUE;
    break;

case WM_CLOSE:
    if (TheGameEngine && !TheGameEngine->getQuitting())
    {
        Bool isAltF4 = s_altF4Requested;
        s_altF4Requested = FALSE;
        ...
    }
```

This flag is set synchronously during the same key-event cycle and is reliable regardless of how long the message pump takes.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (15): Last reviewed commit: "move variable and include to more logica..." | Re-trigger Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@xezon
Copy link

xezon commented Feb 21, 2026

Needs rebase.

My initial thought on this, without looking at code, is that it will make it easier for players to disconnect from a Multiplayer match - by accident or intentional - so this feature must not work during a Multiplayer Match.

@githubawn
Copy link
Author

Will rebase soon.

The multiplayer behavior was designed based on your comment from #1356 (comment).
Rather than blocking Alt+F4 entirely, it intercepts the OS close command to ensure a MSG_SELF_DESTRUCT (Surrender) message is sent over the network before the game quits, so other players aren't left hanging. Happy to adjust if your thinking has changed since then!

It also adds safe abort logic so Alt+F4 works consistently everywhere addressing helmutbuhler's #1356 (comment) concern about the button only working in some situations.

@Caball009
Copy link

Caball009 commented Feb 21, 2026

Thank you for implementing this. I did a few quick tests and the current implementation appears to work well.

@githubawn githubawn force-pushed the fix/graceful-exit-logic branch from 843b370 to 4fda496 Compare February 22, 2026 00:47
@githubawn githubawn force-pushed the fix/graceful-exit-logic branch from 2d896d5 to 036ed84 Compare February 22, 2026 23:35
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been easier to review if this change just contained Core+GeneralsMD edits for starters.

@githubawn githubawn marked this pull request as draft February 26, 2026 23:25
@githubawn githubawn force-pushed the fix/graceful-exit-logic branch from 3a3c926 to 6f2b0b8 Compare February 27, 2026 02:30
@githubawn githubawn force-pushed the fix/graceful-exit-logic branch from a2166e4 to c6ee106 Compare March 3, 2026 22:39
@githubawn githubawn marked this pull request as ready for review March 3, 2026 22:48
@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Additional Comments (1)

Generals/Code/GameEngine/Source/GameClient/GameClient.cpp
TheKeyboard->UPDATE() called on every invocation of isMovieAbortRequested()

isMovieAbortRequested() is called in tight video playback loops (e.g. spinning on isFrameReady() with Sleep(1) between iterations). Each call unconditionally invokes TheKeyboard->UPDATE(), which may accumulate keyboard state incorrectly or cause events to be double-processed when the function is called at high frequency within a single logical frame.

The original code called UPDATE() once per outer loop iteration for a specific keyboard check. The refactored helper should either guard against repeated calls (e.g. a static frame counter check) or the callers should call it once per outer iteration rather than inside the innermost spin loop.

The identical implementation in GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp has the same concern.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameClient/GameClient.cpp
Line: 803-806

Comment:
**`TheKeyboard->UPDATE()` called on every invocation of `isMovieAbortRequested()`**

`isMovieAbortRequested()` is called in tight video playback loops (e.g. spinning on `isFrameReady()` with `Sleep(1)` between iterations). Each call unconditionally invokes `TheKeyboard->UPDATE()`, which may accumulate keyboard state incorrectly or cause events to be double-processed when the function is called at high frequency within a single logical frame.

The original code called `UPDATE()` once per outer loop iteration for a specific keyboard check. The refactored helper should either guard against repeated calls (e.g. a static frame counter check) or the callers should call it once per outer iteration rather than inside the innermost spin loop.

The identical implementation in `GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp` has the same concern.

How can I resolve this? If you propose a fix, please make it concise.

@githubawn githubawn marked this pull request as draft March 3, 2026 23:02
@githubawn githubawn marked this pull request as ready for review March 18, 2026 02:33
moved trystartnewgame to private
fixed MSG_SELF_DESTRUCT sent for single-player loading abort
@xezon xezon changed the title feature(gamelogic): Implement ALT + F4 and Window X button to close game feat: Implement ALT + F4 and Window X button to close game Mar 18, 2026
@xezon xezon added Enhancement Is new feature or request Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour System Is Systems related labels Mar 18, 2026
added comments
moved QuitGameException
@xezon
Copy link

xezon commented Mar 19, 2026

Greptile is still complaining about "the SinglePlayerLoadScreen min-spec wait loop in LoadScreen.cpp was not given the same isMovieAbortRequested() + frame-ready guard that ChallengeLoadScreen received, leaving a small hang window during Generals-only min-spec map loads."

@githubawn
Copy link
Author

Greptile didn't update the first comment and I don't think I can tag it as it doesn't give me the tag pop-up.

Let me try:
@greptileai can you update the first summary at #2336 (comment) ? or if you can't generate a new one.

@greptile-apps
Copy link

greptile-apps bot commented Mar 19, 2026

Summary of PR #2336: Alt+F4 and Window X Button Support

This PR implements graceful exit handling for Alt+F4 and the window close button, addressing issue #1356. The changes are split across two commits:

Commit 1: Centralized Quit Logic

  • New method: GameLogic::quit(Bool toDesktop, Bool force) consolidates all quit handling
  • Replaces scattered code in quit menu callbacks, Alt+F4 handler, and "quit to desktop" button
  • Handles edge cases:
    • Stops active recordings via TheRecorder->stopRecording()
    • Sends self-destruct message in multiplayer (using new m_quitToDesktopAfterMatch flag)
    • Unpauses/unfreezes game before exit via setGamePaused(FALSE) and TheScriptEngine->forceUnfreezeTime()
    • Distinguishes Alt+F4 (force=TRUE) from window X button (force=FALSE, shows quit menu)

Commit 2: Graceful Exit During Loading & Movies

  • Loading screen: LoadScreen::update() now checks quit status and throws QuitGameException, caught in startNewGame() to cleanly abort mid-load
  • Movies: Refactored duplicate ESC-key checking into GameClient::isMovieAbortRequested()
    • Checks ESC key, window close, and Alt+F4 events
    • Applied to video loops in SinglePlayerLoadScreen and ChallengeLoadScreen
    • Window close during movies no longer hangs the game
  • Message system: MSG_META_DEMO_INSTANT_QUIT registered as system message for proper propagation during loading

Technical Details

  • Files changed: 19 files across Generals and GeneralsMD codebases
  • Changes: +373 additions, -139 deletions
  • Key files: GameLogic.cpp, GameClient.cpp, LoadScreen.cpp, WinMain.cpp, QuitMenu.cpp, CommandXlat.cpp
  • Window message handling: WinMain.cpp now detects Alt+F4 vs window X and passes appropriate force flag

The implementation ensures clean shutdown in all game states (main menu, loading, in-game, during movies) while properly handling multiplayer, recording, and script engine cleanup.

@githubawn
Copy link
Author

@greptileai Include "Key findings:" and "Confidence Score:".

@greptile-apps
Copy link

greptile-apps bot commented Mar 19, 2026

Key findings:

  1. Excellent refactoring - The new GameLogic::quit(Bool toDesktop, Bool force) method successfully centralizes previously scattered quit logic from QuitMenu, Alt+F4 handler, and quit buttons. This eliminates code duplication and creates a single source of truth.

  2. Smart force parameter design - The distinction between window X-button (force=FALSE, shows quit menu) vs Alt+F4 (force=TRUE, immediate quit) is well-designed and matches user expectations.

  3. Clean loading abort - The QuitGameException approach elegantly handles mid-loading exits without complex state checking. The try-catch in startNewGame() ensures clean cleanup.

  4. Robust movie abort - Refactoring inline ESC checks into GameClient::isMovieAbortRequested() is cleaner and now properly handles window close/Alt+F4 during videos (previously would hang).

  5. Good defensive programming - Proper null checks on TheInGameUI, TheRecorder, TheScriptEngine, ThePlayerList prevent early-startup/late-shutdown crashes.

  6. System message registration - Moving MSG_META_DEMO_INSTANT_QUIT out of #ifdef RTS_DEBUG and registering it as a system message ensures it propagates correctly during loading.

Minor observations:

  • The m_quitToDesktopAfterMatch flag correctly defers desktop quit in multiplayer until match end
  • Unpause/unfreeze logic before exit prevents game from getting stuck in paused state
  • Both Generals and GeneralsMD codebases updated consistently

Confidence Score: 90/100

This is high-quality work. The architecture is sound, edge cases are handled, and the refactoring meaningfully improves maintainability. The 10% deduction is purely because extensive quit scenarios (loading screens, movies, multiplayer, recording, etc.) require thorough testing to ensure no regressions.

fixes accidentally revert
{
TheMessageStream->appendMessage(GameMessage::MSG_META_DEMO_INSTANT_QUIT);
// TheSuperHackers @info ThePlayerList initialized ensures we can safely append messages.
// If not, we're likely in early startup/late shutdown, so we hard-quit instead.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the comment describes is unintuitive. Where/how have you seen this requirement?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2336 (comment)

this was added after this comment


//This method used to disable quitting. We just put up the options screen instead.
//TheMessageStream->appendMessage(GameMessage::MSG_META_OPTIONS);
if (TheMessageStream && ThePlayerList)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is also testing ThePlayerList. I still do not understand it. It looks like a "code smell".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the exact same reason as above. It prevents the engine from crashing during an early exit since ThePlayerList isn't ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Is new feature or request Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker System Is Systems related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement ALT + F4 and Window X button to close game

5 participants